/* Js2JavadocTask.java
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

package dowry.util;

import java.io.*;
import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.taskdefs.*;
import org.apache.tools.ant.types.Commandline.*;

public class Js2JavadocTask extends Task
{
  // source files to be processed
  private FileSet fileSet = null;

  // the destination directory
  private File destDir = null;

  /**
   * Returns text of file.
   *
   * @param fileName String  Fully qualified fileName.
   * @throws IOException
   * @throws FileNotFoundException
   * @return String  Text of the input file.
   */
  private String getFileText(String fileName) throws IOException,
      FileNotFoundException
  {
    BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(
        fileName)));

    // Container for entire contents of "file" body.
    StringBuffer fileStringBuffer = new StringBuffer();
    String line;
    while ( (line = bufferedReader.readLine()) != null)
    {
      fileStringBuffer.append(line + "\n");
    }

    return fileStringBuffer.toString();

  }

  /**
   *
   * @param packageName String  Directory where file will be written.
   * @param fileName String  Base filename without an extension.
   * @param fileText String  Text of the newly created file.
   * @throws Exception
   */
  private void writeProcessedTextToJavaFile(String packageName, String fileName,
                                            String fileText) throws Exception
  {
    String newPackage = this.destDir + File.separator +
        packageName + File.separator;

    // Create directory for this new package.
    File newDirectory = new File(newPackage);
    if (!newDirectory.exists())
    {
      boolean madeDir = newDirectory.mkdirs();
    }

    String javaFileName = fileName + ".java";
    FileWriter fileWriter = new FileWriter(newPackage + javaFileName);
    fileWriter.write(fileText);
    fileWriter.flush();
    fileWriter.close();

  }

  /**
   * Handles all the necessary operations to make a JavaScript file "JavaDocable".
   *
   * @param baseDir String  Root directory for source.
   * @param fileName String  Directory above base dir and fileName.
   */
  private void process(String baseDir, String fileName)
  {
    try
    {
      String packageName = "";
      String className = "";

      // This file is in a sub-package.
      if (fileName.indexOf("\\") >= 0)
      {
        packageName = fileName.substring(0, fileName.lastIndexOf("\\"));
        className = fileName.substring(fileName.lastIndexOf("\\") +
            1,
            fileName.lastIndexOf("."));
      }
      else
      {
        className = fileName.substring(0,
            fileName.lastIndexOf("."));
      }

      String fileText = getFileText(baseDir + "\\" + fileName);

      String packageNameWithPeriods = packageName.replace('\\', '.');
      String processedText = makeConsumableForJavadoc(packageNameWithPeriods,
          className,
          fileText);

      writeProcessedTextToJavaFile(packageName,
                                   className,
                                   processedText);
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }

  }

  /**
   * returns true if input is a JavaScript comment line
   *
   * @param line String line of JavaScript
   * @return boolean
   */
  private boolean isCommentsLine(String line)
  {
    boolean returning = false;

    if (isFirstCommentsLine(line) || inMultiLineComments(line))
    {
      returning = true;
    }
    return returning;
  }

  /**
   * returns true if input is a Javascript (DOWRY) import statement
   *
   * @param line String line of JavaScript
   * @return boolean
   */
  private boolean isPackageImport(String line)
  {
    boolean returning = false;

    if (line.trim().startsWith("$import"))
    {
      returning = true;
    }
    return returning;
  }

  /**
   * returns true if input is a JavaScript single-line comment or
   * the first line of a multi-liner
   *
   * @param line String line of JavaScript
   * @return boolean
   */
  private boolean isFirstCommentsLine(String line)
  {
    boolean returning = false;

    if (line.trim().startsWith("//") ||
        line.trim().startsWith("/*"))
    {
      returning = true;
    }
    return returning;

  }

    /**
     * returns true if input is a JavaScript multi-line comment or
     * the last line of a multi-liner
     *
     * @param line String line of JavaScript
     * @return boolean
     */
  private boolean inMultiLineComments(String line)
  {
    boolean returning = false;

    if ( (line.trim().startsWith("*")) ||
         (line.trim().endsWith("*/")))
    {
      returning = true;
    }
    return returning;
  }

  /**
   * returns true if input is a JavaScript class declaration
   *
   * @param line String  line of JavaScript
   * @param fileName String  fileName without extension
   * @return boolean
   */
  private boolean isClassDeclaration(String line, String fileName)
  {
    boolean returning = false;

    String trimmed = line.trim();
    if (trimmed.indexOf("$class(\"") >= 0)
    {
      returning = true;
    }
    return returning;

  }

  /**
   * returns true if input is a JavaScript class declaration
   *
   * @param line String  line of JavaScript
   * @param fileName String  fileName without extension
   * @return boolean
   */
  private boolean isMixinDeclaration(String line, String fileName)
  {
    boolean returning = false;

    String trimmed = line.trim();
    if (trimmed.indexOf("$mixin(\"") >= 0)
    {
      returning = true;
    }
    return returning;

  }

  /**
   * returns true if input is a JavaScript function declaration
   *
   * @param line String  line of JavaScript
   * @param fileName String  fileName without extension
   * @return boolean
   */
  private boolean isFunctionDeclaration(String line)
  {
    boolean returning = false;

    String trimmed = line.trim();
    if ( (trimmed.indexOf("function") >= 0) &&
         ((trimmed.indexOf("//") < 0) || (trimmed.indexOf("function") <= trimmed.indexOf("//"))) &&
         (!trimmed.trim().startsWith("return")) &&
         (trimmed.indexOf("Function.prototype.callback") < 0) &&
         (trimmed.indexOf("=") < 0) )
    {
      returning = true;
    }
    return returning;

  }

  /**
   * returns true if input is a JavaScript class variable declaration
   *
   * @param line String  line of JavaScript
   * @param lastLine String  previous JavaScript line
   * @return boolean
   */
  private boolean isClassVariable(String line, String lastLine)
  {
    boolean returning = false;

    String trimmed = line.trim();
    String trimmedLast = lastLine.trim();

    if ((trimmed.indexOf(":") > 0) &&
        (!trimmed.endsWith(";")) &&
        (!trimmed.startsWith("else")) &&
        (!trimmed.startsWith("if")) &&
        (!trimmed.startsWith("var")) &&
        ((trimmedLast.length() == 0) ||
        (trimmedLast.endsWith("*/")) ||
        (trimmedLast.startsWith("//"))) )
    {
      returning = true;
    }
    return returning;

  }

  /**
   * returns Java-ish class/interface declaration line
   *
   * @param line String  line of JavaScript
   * @param fileName String  fileName without extension
   */
  private String getClassInterfaceDeclaration(String line, String fileName, boolean isInterface)
  {
    StringBuffer returning = new StringBuffer();

    String superClass = null;
    String implementedClass = null;
    String extendsToken = ".$extends(\"";
    String withToken = ".$with(\"";
    int extendsIndex = line.indexOf(extendsToken);
    int withIndex = line.indexOf(withToken);
    if (extendsIndex > 0)
    {
      superClass = line.substring(extendsIndex + extendsToken.length(),
                                  line.indexOf("\").$", extendsIndex));
    }

    if (withIndex > 0)
    {
      implementedClass = line.substring(withIndex + withToken.length(),
                                  line.indexOf("\").$", withIndex));
    }

    if (isInterface)
    {
        returning.append("public interface " + fileName);
    }
    else
    {
        returning.append("public class " + fileName);
    }

    if (superClass != null)
    {
        returning.append(" extends " + superClass);
    }

    if (implementedClass != null)
    {
        returning.append(" implements " + implementedClass);
    }

    returning.append(" {\n");

    return returning.toString();

  }

  /**
   * return a Java-ish function declaration
   *
   * @param line String
   * @param functionReturnType String
   * @param className String
   * @return String
   */
  private String getFunctionDeclaration(String line, String functionReturnType, String className)
  {
    StringBuffer returning = new StringBuffer();

    String parmString = line.substring(line.indexOf("(") + 1,
                                         line.indexOf(")"));

    // Array of function's input parameters.
    String[] parms = parmString.split(",");
    String functionName = null;
    if (line.indexOf(":") > 0)
    {
      functionName = line.split(":")[0].trim();
    }
    else
    {
      String functionToken = "function ";
      functionName = line.substring(functionToken.length(), line.indexOf("(")).trim();
    }

    String functionScope = "public";
    if (functionName.startsWith("_"))
    {
      functionScope = "protected";
    }

    // This is the constructor.  Constructors do not have return types.
    if (functionName.startsWith(className))
    {
        returning.append(functionScope + " " + functionName + "(");
    }
    else
    {
        returning.append(functionScope + " " + functionReturnType + " " + functionName + "(");
    }

    // This "if" is necessary to determine if this function has a parameter.
    String separator = "";
    if (parmString.trim().length() > 0)
    {
      for (int j = 0; j < parms.length; j++)
      {
        returning.append(separator);
        returning.append("Object " + parms[j]);
        separator = ", ";
      }
    }
    returning.append(") {}\n");

    return returning.toString();

  }

  /**
   * returns a Java-ish class variable
   *
   * @param line String
   * @return String
   */
  private String getClassVariable(String line)
  {
    StringBuffer returning = new StringBuffer();

    String variableName = line.trim().split(":")[0].trim();

    String variableAccess = "public";
    if (line.trim().startsWith("_"))
    {
      variableAccess = "protected";
    }
    returning.append(variableAccess + " Object " + variableName + ";\n");

    return returning.toString();

  }

  /**
   * Handles embedded <fileset> tag in the build.xml.
   *
   * @param fileSet FileSet
   */
  public void add(FileSet fileSet)
  {
    this.fileSet = fileSet;
  }

  /**
   * Setter for "destDir" attribute of this custom Ant task.
   *
   * @param destDir File
   */
  public void setDestDir(File destDir)
  {
    this.destDir = destDir;
  }

  /**
   * The Ant Task's "main".
   */
  public void execute()
  {
    Project project = getProject();
    File baseDir = fileSet.getDir(project);
    DirectoryScanner directoryScanner = fileSet.getDirectoryScanner(project);
    String[] fileNames = directoryScanner.getIncludedFiles();

    try
    {
      String dowryPackage = this.destDir + File.separator;

    File dowryDir = new File(dowryPackage);
    if (!dowryDir.exists())
    {
      dowryDir.mkdir();
    }

      for (int i = 0; i < fileNames.length; i++)
      {
        process(baseDir.toString(), fileNames[i]);
      }
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }

  }

  /**
   * Transforms the JavaScript input to a Java-ish format
   * that JavaDoc can handle.
   *
   * @param packageName String
   * @param fileName String
   * @param fileText String
   * @return String  The new, transformed Java-ish document.
   */
  public String makeConsumableForJavadoc(String packageName, String fileName, String fileText)
  {
    // Holds modified "Javadocable" text that this function will return.
    StringBuffer changedFileText = new StringBuffer();

    // Each line in the file is an element in this array.
    String[] lines = fileText.split("\n");

    String packageDeclarationLine = "";

    // This file is in a sub-package, so overwrite "packageDeclarationLine".
    if (packageName.length() > 0)
    {
      packageDeclarationLine = "package " + packageName + ";\n";
    }
    changedFileText.append(packageDeclarationLine);

    boolean classDeclared = false;

    // Holder for text of a function.  Added to support multiline function declarations.
    StringBuffer functionDeclaration = new StringBuffer();

    String functionReturnType = "void";
    String lastLine = "";

    // Loop through all the lines in the file.
    for (int i = 0; i < lines.length; i++)
    {

       if (isCommentsLine(lines[i]))
       {
         if (isFirstCommentsLine(lines[i]))
         {
           // "Reset" this variable.
           functionReturnType = "void";
         }

         if (lines[i].indexOf("@return") >= 0)
         {
           functionReturnType = "Object";
         }

         changedFileText.append(lines[i] + "\n");

       }

       else if (isPackageImport(lines[i]))
       {
           String importedPackage = lines[i].substring(lines[i].indexOf("$import(\"") + 9, lines[i].indexOf("\");"));
           changedFileText.append("import " + importedPackage + ";\n");
       }

       else if ( !classDeclared && (isClassDeclaration(lines[i], fileName) || isMixinDeclaration(lines[i], fileName)) )
       {
         classDeclared = true;
         changedFileText.append(getClassInterfaceDeclaration(lines[i], fileName, isMixinDeclaration(lines[i], fileName) ));
       }

       else if ( (isFunctionDeclaration(lines[i])) || (functionDeclaration.length() > 0) )
       {
         functionDeclaration.append(lines[i].trim());

         // Complete, single-line function declaration or end of multi-line function.
         if (lines[i].indexOf(")") > 0)
         {
           changedFileText.append(getFunctionDeclaration(functionDeclaration.
                 toString(), functionReturnType, fileName));
           functionDeclaration = new StringBuffer();
         }

       }

       else if (isClassVariable(lines[i], lastLine))
       {
         changedFileText.append(getClassVariable(lines[i]));
       }

       lastLine = lines[i];
     }
     // End:  Loop through all the lines in the file.

     // Difficult to always catch the class declaration above, so this "if" is a catch-all
     // to declare the class.
     if (!classDeclared)
     {
       int index = changedFileText.indexOf(packageDeclarationLine) + packageDeclarationLine.length();
       String classDeclarationLine = "public class " + fileName + " {\n";
       changedFileText.insert(index, classDeclarationLine);
     }

    changedFileText.append("}\n");

    return changedFileText.toString();
  }

}
